package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面。实现公共字段的自动填充逻辑
 */
@Slf4j
@Component // 加上该注解表示将当前类交给spring管理 成为IOC容器中的bean对象
@Aspect // 加上该注解表示将当前类声明为切面类（AOP类）
public class AutoFillAspect {

    // 切入点表达式
    // 该切入点表达式表示匹配返回类型是任意的 且是com.sky.mapper下的所有的类或者接口所有的方法 方法的参数也是任意的  并且加上了@AutoFill注解的方法
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    // Spring提供了@PointCut注解，该注解的作用是将公共的切入点表达式抽取出来，需要用到时引用该切入点表达式即可。
    public void autoFillPointCut() {
    }

    /**
     * 前置通知,在通知中进行公共字段的赋值
     *
     * @param joinPoint
     */
    @Before("autoFillPointCut()") // 这里需要使用前置通知我们需要在insert 和 update方法执行之前填充公共字段 所以使用前置通知
    // 注意使用除了环绕通知之外 其他的通知必须使用JoinPoint作为参数
    public void autoFill(JoinPoint joinPoint) {
        log.info("开始进行公共字段自动填充...");

        // 思路
        // 1.获取当前被拦截的方法上的数据库操作类型是insert还是update
        // 向下转型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 获取方法签名对象
        // 先获得方法对象，在获得方法上的注解对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解对象
        OperationType operationType = autoFill.value();//获取数据库操作类型

        // 2.获取到当前被拦截的方法的参数 --参数是一个实体对象
        Object[] args = joinPoint.getArgs(); // 做一个约定：被拦截的方法把实体对象放在第一个位置
        //判断防止空指针
        if (args == null || args.length == 0) {
            return;
        }
        Object entity = args[0]; //获取第一个参数

        // 3.为实体对象准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        // 4.根据当前不同的操作类型，为对应的属性通过反射来赋值
        if (operationType == OperationType.INSERT) {
            // 如果是insert 为4个公共字段赋值
            try {
                // 通过反射 获取方法对象    这里定义了一个常量把我们的几个方法名定义成了常量防止出错
                // entity.getClass() 获取实体对象的字节码对象.getDeclaredMethod()表示获取到方法对象
                // 第一个参数是指定获取哪一个方法的方法名称 第二个参数是方法参数的类型对象
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);


                // 通过反射给属性赋值
                // 通过调用invoke方法来触发方法的执行 第一个参数是要调用的方法对象 第二个参数是调用方法时传递的参数
                setCreateTime.invoke(entity, now); //通过调用这个方法，可以动态地为entity对象的createTime属性设置当前时间。
                setCreateUser.invoke(entity, currentId);
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else if (operationType == OperationType.UPDATE) {
            // 如果是update 为2个公共字段赋值
            try {
                // 通过反射 获取方法对象
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                // 通过反射给属性赋值
                // 通过调用invoke方法来触发方法的执行 第一个参数是要调用的方法对象 第二个参数是调用方法时传递的参数
                setUpdateTime.invoke(entity, now); // 通过调用这个方法，可以动态地为entity对象的createTime属性设置当前时间。
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
